Skip to content

测试与混沌测试

基于 rts-server-golang sim/、cmd/ 目录解析 关键词: 确定性测试, 混沌注入, Fuzzing, 回归测试, 网络模拟

概述

RTS 游戏服务器的测试有三个特殊难点:

  1. 确定性:同样的输入必须产生同样的结果
  2. 网络不可靠:UDP 会丢包、乱序、延迟
  3. 时间相关:tick 推进依赖时间流逝

本项目通过三类测试覆盖这些场景。

确定性测试

为什么确定性测试特殊

普通测试:

go
got := Add(2, 3)
if got != 5 { t.Error(...) }  // 结果确定

确定性测试需要多次运行,验证结果一致:

go
hashes := make([]uint64, 100)
for i := range hashes {
    w := setupTestWorld()
    Step(w, cmds)  // 同初始状态、同命令
    hashes[i] = Hash(w)
}
allSame(hashes)  // 100次结果必须完全相同

TestHashDeterminism

验证:相同初始状态 + 相同命令序列 → 100 次 hash 完全一致。

go
func TestHashDeterminism(t *testing.T) {
    hashes := make([]uint64, 100)
    for i := range hashes {
        w := setupTestWorld()
        cmds := []Cmd{
            {Player: 0, Op: CmdMove, UnitID: 1, TargetPos: fixed.VInt(50, 50)},
            {Player: 1, Op: CmdAttack, UnitID: 4, TargetID: 1},
        }
        for tick := 0; tick < 200; tick++ {
            if tick == 0 { Step(w, cmds) } else { Step(w, nil) }
        }
        hashes[i] = Hash(w)
    }
    for i := 1; i < len(hashes); i++ {
        if hashes[i] != hashes[0] {
            t.Fatalf("hash mismatch at run %d", i)
        }
    }
}

失败场景:如果 Step() 中用了浮点数、随机数、或遍历顺序不确定,hash 会发散。

TestSnapshotRoundTrip

验证:Marshal → Unmarshal → 继续跑,结果仍然一致。

go
w := setupTestWorld()
Step(w, cmds)
data := Marshal(w)
w2, _ := Unmarshal(data)

// hash 必须完全一致
if Hash(w) != Hash(w2) { t.Fatal("hash mismatch after roundtrip") }

// 继续跑 100 tick,hash 仍然一致
for i := 0; i < 100; i++ {
    Step(w, nil)
    Step(w2, nil)
}
if Hash(w) != Hash(w2) { t.Fatal("hash diverged") }

TestFixedDeterminism

验证固定点数学的确定性:

go
func TestDeterminism(t *testing.T) {
    var results [100]int32
    for i := range results {
        a := FromFloat64(3.14159)
        b := FromFloat64(2.71828)
        c := a.Mul(b).Add(Sqrt(a)).Sub(b.Div(a))
        results[i] = (c + Sin(a).Mul(Cos(b))).Raw()
    }
    for i := 1; i < len(results); i++ {
        if results[i] != results[0] {
            t.Fatalf("determinism broken: run %d", i)
        }
    }
}

固定点数学测试

基本运算

go
func TestMulDiv(t *testing.T) {
    a := FromFloat64(3.5)
    b := FromFloat64(2.0)
    mul := a.Mul(b)  // 3.5 * 2.0 = 7.0
    div := a.Div(b)  // 3.5 / 2.0 = 1.75
}

Sqrt — 二分查找

go
func TestSqrt(t *testing.T) {
    // 手动构造测试用例
    // 2 的平方根 ≈ 1.4142,误差容忍 0.01
    got := Sqrt(FromFloat64(2)).ToFloat64()
    if math.Abs(got-1.4142) > 0.01 { t.Error(...) }
}

Vec2 — 距离计算

go
func TestVec2MoveToward(t *testing.T) {
    from := VInt(0, 0)
    target := VInt(10, 0)
    maxDist := FromInt(3)

    result := MoveToward(from, target, maxDist)
    // 3/10 的距离 → x = 3
    if result.X.ToFloat64() != 3.0 { t.Error(...) }

    // 距离不足时直接到目标
    from2 := V(FromFloat64(9.5), FromInt(0))
    result2 := MoveToward(from2, target, maxDist)
    if result2.X != target.X { t.Error(...) }
}

混沌测试 (chaos-shim)

chaos-shim 是独立的 UDP 代理,夹在客户端和服务端之间,注入网络故障:

客户端 → chaos-shim(:9002) → 服务端(:9000)
         ↑ 注入丢包/延迟/乱序

启动方式

bash
./chaos-shim \
  -listen :9002        \  # 代理监听端口
  -forward 127.0.0.1:9000 \  # 真实服务端
  -drop 0.05           \  # 5% 丢包率
  -delay 50            \  # 基础延迟 50ms
  -jitter 20           \  # 抖动 ±20ms

注入逻辑

go
chaosForward := func(data []byte, dst *net.UDPAddr) {
    // 1. 丢包
    if rng.Float64() < *dropPct {
        dropped++
        return
    }

    // 2. 延迟 + 抖动
    delay := time.Duration(*delayMs) * time.Millisecond
    delay += time.Duration(rng.Intn(*jitterMs*2)-*jitterMs) * time.Millisecond

    // 异步发送
    go func() {
        time.Sleep(delay)
        conn.WriteToUDP(data, dst)
    }()
}

验证内容

故障类型验证
丢包 5%重传机制正常工作
延迟 50±20msAdaptiveN 能适应变化
抖动RTT 估算准确,RTO 合理
组合断线重连能成功恢复

回归测试建议

对于实际项目,建议添加:

bash
# 确定性回归
go test -count=100 ./sim/...

# 对抗模式混沌测试
./chaos-shim -drop=0.1 -delay=100 &
./fake-client -players=2 -ticks=1000
./server -tick-rate=20

# 并发安全
go test -race ./...

相关

撰写